package fr.machizaud.research.handlers;

import fr.machizaud.research.FCPGenerator;
import fr.machizaud.research.MalformedDocumentException;
import fr.machizaud.research.XMLHandler;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.xpath.XPath;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;

/**
 * User: Andréa
 * Date: 23/01/11
 * Time: 17:46
 * <p/>
 * Merge given document with a FCP Sequence template
 */
public class FCPSequenceMerger
        implements XMLHandler {
    private static final java.util.logging.Logger logger =
            java.util.logging.Logger.getLogger(FCPSequenceMerger.class.getCanonicalName());

    public static final String[] ATTRIBUTES = new String[]{
            "id"
    };

    public static final String[] NODES = new String[]{
            "name",
            "duration"
    };

    public static final String[] RATE_NODES = new String[]{
            "ntsc",
            "timebase"
    };

    private FCPSequenceMerger() {
    }

    private static class FCPSequenceMergerHolder {
        private static final FCPSequenceMerger instance = new FCPSequenceMerger();
    }

    public static FCPSequenceMerger getInstance() {
        return FCPSequenceMergerHolder.instance;
    }

    @Override
    public void process(Document dom) throws MalformedDocumentException {
        logger.log(Level.INFO, "Handler {0}", getClass().getName());
        //Set some stuff for document builder
        SAXBuilder builder = new SAXBuilder(true);
        builder.setProperty(
                "http://java.sun.com/xml/jaxp/properties/schemaLanguage",
                "http://www.w3.org/2001/XMLSchema"
        );
        builder.setProperty(
                "http://java.sun.com/xml/jaxp/properties/schemaSource",
                new File(
                        FCPGenerator.XSD_PATH
                )
        );

        //get the template
        try {
            Document dummy = new SAXBuilder().build(getClass().getClassLoader().getResourceAsStream("empty-sequence.xml"));
            Document fcpDom = builder.build(
                    getClass().getClassLoader().getResourceAsStream("empty-sequence.xml")
            );

            mergeSequenceData(dom, fcpDom);

            mergeMediaData(dom, fcpDom);

            //New dom is merged dom
            dom.setRootElement((Element) fcpDom.getRootElement().detach());
        } catch (JDOMException e) {
            throw new MalformedDocumentException("JDOMException", getClass(), e);
        } catch (IOException e) {
            throw new MalformedDocumentException("IOException", getClass(), e);
        }

        logger.log(Level.FINER, "Handler {0} : resulting dom {1}",
                new Object[]{
                        getClass(), dom
                }
        );
    }

    /**
     * Merge sequence according to Final Cut Pro XML Interchange Format
     * available at
     * http://developer.apple.com/library/mac/documentation/AppleApplications/Reference/FinalCutPro_XML/Elements/Elements.html#//apple_ref/doc/uid/TP30001156-TPXREF104
     * http://developer.apple.com/library/mac/documentation/AppleApplications/Reference/FinalCutPro_XML/Elements/Elements.html#//apple_ref/doc/uid/TP30001156-TPXREF107
     *
     * @param dom    Document coming from atl transformation
     * @param fcpDom Document coming from fcp template
     * @throws MalformedDocumentException If cannot detect researched node using XPath
     */
    private void mergeSequenceData(Document dom, Document fcpDom) throws MalformedDocumentException {
        try {
            logger.fine("Merging sequence data...");
            Element fcpSequence = (Element) XPath.selectSingleNode(fcpDom.getRootElement(), "//sequence");
            Element atlSequence = (Element) XPath.selectSingleNode(dom.getRootElement(), "//sequence");

            //merging value
            for (String attribute : ATTRIBUTES) {
                logger.log(Level.FINE, "Merging " + attribute + " value : {0}",
                        atlSequence.getAttributeValue(attribute));
                fcpSequence.getAttribute(attribute).setValue(
                        atlSequence.getAttributeValue(attribute)
                );
            }

            //merging name
            for (String node : NODES) {
                logger.log(Level.FINE, "Merging " + node + " value : {0}",
                        atlSequence.getChildText(node));
                fcpSequence.getChild(node).setText(
                        atlSequence.getChildText(node)
                );
            }

            for (String rate_node : RATE_NODES) {
                logger.log(Level.FINE, "Merging rate." + rate_node + " value : {0}",
                        atlSequence.getChild("rate").getChildText(rate_node));
                fcpSequence.getChild("rate").getChild(rate_node).setText(
                        atlSequence.getChild("rate").getChildText(rate_node)
                );
            }

            //merging timecode
            logger.log(Level.FINE, "Merging timecode.frame value : {0}",
                    atlSequence.getChildText("duration"));
            fcpSequence.getChild("timecode").getChild("frame").setText(
                    atlSequence.getChildText("duration")
            );

            //Default value given to 'displayformat' is NDF ("Non-drop frame").
            // cf. http://developer.apple.com/library/mac/#documentation/AppleApplications/Reference/FinalCutPro_XML/Elements/Elements.html#//apple_ref/doc/uid/TP30001156-DontLinkElementID_207
            logger.log(Level.FINE, "Setting timecode.displayformat value to default : {0}",
                    "NDF");
            fcpSequence.getChild("timecode").getChild("displayformat").setText(
                    "NDF"
            );


            for (String rate_node : RATE_NODES) {
                logger.log(Level.FINE, "Merging timecode.rate." + rate_node + " value : {0}",
                        atlSequence.getChild("rate").getChildText(rate_node));
                fcpSequence.getChild("timecode").getChild("rate").getChild(rate_node).setText(
                        atlSequence.getChild("rate").getChildText(rate_node)
                );
            }

            //let in/out to default (-1)
            logger.log(Level.FINE, "Letting in/out value to default : -1");

        } catch (JDOMException e) {
            throw new MalformedDocumentException("JDOMException", getClass(), e);
        }
    }

    /**
     * Merge sequence according to Final Cut Pro XML Interchange Format
     * available at
     * http://developer.apple.com/library/mac/documentation/AppleApplications/Reference/FinalCutPro_XML/Elements/Elements.html#//apple_ref/doc/uid/TP30001156-TPXREF109
     * http://developer.apple.com/library/mac/documentation/AppleApplications/Reference/FinalCutPro_XML/Elements/Elements.html#//apple_ref/doc/uid/TP30001156-TPXREF105
     *
     * @param dom    Document coming from atl transformation
     * @param fcpDom Document coming from fcp template
     * @throws MalformedDocumentException If cannot detect researched node using XPath
     */
    private void mergeMediaData(Document dom, Document fcpDom) throws MalformedDocumentException {
        try {
            logger.fine("Merging video data...");
            Element fcpVideo = (Element) XPath.selectSingleNode(fcpDom.getRootElement(), "sequence/media/video");
            Element atlVideo = (Element) XPath.selectSingleNode(dom.getRootElement(), "sequence/media/video");

            logger.fine("Merging format...");
            fcpVideo.getChild("format").getChild("samplecharacteristics").getChild("width").setText(
                    atlVideo.getChild("format").getChild("samplecharacteristics").getChildText("width")
            );
            fcpVideo.getChild("format").getChild("samplecharacteristics").getChild("height").setText(
                    atlVideo.getChild("format").getChild("samplecharacteristics").getChildText("height")
            );

            logger.fine("Adding clipitems...");
            Element fcpTrack = fcpVideo.getChild("track");
            List atlClipitems = XPath.selectNodes(atlVideo.getChild("track"), "clipitem");

            Map<String, Element> clipitemsMap = listToMap(atlClipitems);

            for (Element clipitem : clipitemsMap.values()) {
                fcpTrack.addContent(
                        (Element) clipitem.clone()
                );
            }

            logger.fine("Merging audio data...");
            Element fcpAudio = (Element) XPath.selectSingleNode(fcpDom.getRootElement(), "sequence/media/audio");
            Element atlAudio = (Element) XPath.selectSingleNode(dom.getRootElement(), "sequence/media/audio");

            fcpTrack = fcpAudio.getChild("track");
            atlClipitems = XPath.selectNodes(atlAudio.getChild("track"), "clipitem");

            for (Object clipitemObject : atlClipitems) {
                if (clipitemObject instanceof Element) {
                    Element clipitem = (Element) clipitemObject;

                    fcpTrack.addContent(
                            (Element) clipitemsMap.get(
                                    clipitem.getAttributeValue("id")
                            ).clone()
                    );
                }
            }

        } catch (JDOMException e) {
            throw new MalformedDocumentException("JDOMException", getClass(), e);
        }

    }

    /**
     * Nice method to transform list of clipitem to a map with 'id' as key
     *
     * @param clipitems list of clipitems
     * @return map of clipitem with 'id' as key
     */
    private Map<String, Element> listToMap(List clipitems) {
        HashMap<String, Element> map = new HashMap<String, Element>(clipitems.size());
        for (Object clipitemObject : clipitems) {
            if (clipitemObject instanceof Element) {
                Element clipitem = (Element) clipitemObject;
                map.put(clipitem.getAttributeValue("id"), clipitem);
            }
        }
        return map;
    }


}
